rns.moscow 🟥 [git]
Commit 5fc5be0a3a97bf8e793fe9e92ffffac45a4b6fd2
Parents : d1666b7
Author : Nickie Deuxyeux <nikolay@dvoeglazov.ru>
Date : 2026-07-05T19:20:12+03:00
map: make the hover tooltip uplink/downlink-aware
The hover tooltip only ever showed uplink RSSI/SNR, regardless of which
overlay mode was active. It now matches the current mode:
- Uplink: unchanged, searches all points.
- Downlink: shows the downlink reading, and only matches points that
actually have one, so hovering finds whatever's really rendered on the
downlink heat layer instead of an uplink-only point nearby.
- Uplink+Downlink: shows both gauges stacked (downlink below uplink) when
the hovered point has both; downlink row hides gracefully if that point
has no downlink reading.
Required adding dl_rssi/dl_snr to /api/points (the endpoint the tooltip's
point set already came from) since both live on the same row per point —
no second query needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Changes
Diff
diff --git a/map/map_server.py b/map/map_server.py
index d1b9e84..7f2d64b 100755
--- a/map/map_server.py
+++ b/map/map_server.py
@@ -318,8 +318,9 @@ body.light-theme #filter-apply { background: #e8f5e8; border-color: #4a8a4a; col
background: var(--bg); border: 1px solid var(--border2); border-radius: 3px;
padding: 4px 6px;
}
- #hover-bars { display: flex; gap: 4px; align-items: center; }
- #hover-bars > .vbar-col { font-size: 7px; font-family: 'Courier New', monospace; white-space: nowrap; position: relative; top: 2px; }
+ .hover-row { display: flex; gap: 4px; align-items: center; }
+ .hover-row > .vbar-col { font-size: 7px; font-family: 'Courier New', monospace; white-space: nowrap; position: relative; top: 2px; }
+ #hover-bars-dl { margin-top: 3px; padding-top: 3px; border-top: 1px solid var(--border2); }
.vbar-wrap {
width: 4px; height: 18px; position: relative;
background: linear-gradient(to top, #c33, #cc3, #3a3);
@@ -484,7 +485,7 @@ body.light-theme #filter-apply { background: #e8f5e8; border-color: #4a8a4a; col
<body>
<div id="discovery-warning">⚠️ Discovery data unavailable — node list may be incomplete or out of date (rnsd not responding).</div>
<div id="hover-tip">
- <div id="hover-bars">
+ <div id="hover-bars" class="hover-row">
<div class="vbar-col">
<span id="hover-rssi-val"></span>
<span class="vbar-lbl">dBm</span>
@@ -496,6 +497,18 @@ body.light-theme #filter-apply { background: #e8f5e8; border-color: #4a8a4a; col
<span class="vbar-lbl">dB</span>
</div>
</div>
+ <div id="hover-bars-dl" class="hover-row">
+ <div class="vbar-col">
+ <span id="hover-dl-rssi-val"></span>
+ <span class="vbar-lbl">dBm</span>
+ </div>
+ <div class="vbar-wrap"><div class="vbar-mask" id="hover-dl-rssi-mask"></div></div>
+ <div class="vbar-wrap"><div class="vbar-mask" id="hover-dl-snr-mask"></div></div>
+ <div class="vbar-col">
+ <span id="hover-dl-snr-val"></span>
+ <span class="vbar-lbl">dB</span>
+ </div>
+ </div>
</div>
<div id="nodes-open-wrap"><button id="nodes-open" title="Node List">📑 Node List</button></div>
<div id="nodes-panel">
@@ -665,8 +678,6 @@ const bounds = [];
const allPoints = [];
const hoverTip = document.getElementById('hover-tip');
-const rssiMask = document.getElementById('hover-rssi-mask');
-const snrMask = document.getElementById('hover-snr-mask');
function pctToColor(pct) {
const t = Math.min(1, Math.max(0, pct / 100));
@@ -681,21 +692,40 @@ function pctToColor(pct) {
}
}
-function showHoverTip(p, cx, cy) {
- const rssiPct = p.rssi !== null ? Math.min(100, Math.max(0, (p.rssi + 110) / 80 * 100)) : 0;
- const snrPct = p.snr !== null ? Math.min(100, Math.max(0, (p.snr + 20) / 30 * 100)) : 0;
- rssiMask.style.height = (100 - rssiPct) + '%';
- snrMask.style.height = (100 - snrPct) + '%';
- const rssiEl = document.getElementById('hover-rssi-val');
- const snrEl = document.getElementById('hover-snr-val');
+// Fills one gauge row (idPrefix '' = uplink, 'dl-' = downlink) from rssi/snr, which may be
+// null/undefined (no reading for this direction on this point) — rendered as empty/grey.
+function setGaugeRow(idPrefix, rssi, snr) {
+ const hasRssi = rssi !== null && rssi !== undefined;
+ const hasSnr = snr !== null && snr !== undefined;
+ const rssiPct = hasRssi ? Math.min(100, Math.max(0, (rssi + 110) / 80 * 100)) : 0;
+ const snrPct = hasSnr ? Math.min(100, Math.max(0, (snr + 20) / 30 * 100)) : 0;
+ document.getElementById(`hover-${idPrefix}rssi-mask`).style.height = (100 - rssiPct) + '%';
+ document.getElementById(`hover-${idPrefix}snr-mask`).style.height = (100 - snrPct) + '%';
+ const rssiEl = document.getElementById(`hover-${idPrefix}rssi-val`);
+ const snrEl = document.getElementById(`hover-${idPrefix}snr-val`);
const rssiLbl = rssiEl.nextElementSibling;
const snrLbl = snrEl.nextElementSibling;
- rssiEl.textContent = p.rssi !== null ? p.rssi : '';
- snrEl.textContent = p.snr !== null ? p.snr : '';
- const rc = p.rssi !== null ? pctToColor(rssiPct) : '#555';
- const sc = p.snr !== null ? pctToColor(snrPct) : '#555';
- rssiEl.style.color = rc; rssiLbl.style.color = rc;
- snrEl.style.color = sc; snrLbl.style.color = sc;
+ rssiEl.textContent = hasRssi ? rssi : '';
+ snrEl.textContent = hasSnr ? snr : '';
+ const rc = hasRssi ? pctToColor(rssiPct) : '#555';
+ const sc = hasSnr ? pctToColor(snrPct) : '#555';
+ rssiEl.style.color = rc; rssiLbl.style.color = rc;
+ snrEl.style.color = sc; snrLbl.style.color = sc;
+}
+
+const hoverBarsUl = document.getElementById('hover-bars');
+const hoverBarsDl = document.getElementById('hover-bars-dl');
+
+// showUl/showDl reflect the current overlay mode; the downlink row additionally only shows
+// when the hovered point actually has a downlink reading (not every point does).
+function showHoverTip(p, cx, cy, showUl, showDl) {
+ if (showUl) { setGaugeRow('', p.rssi, p.snr); hoverBarsUl.style.display = 'flex'; }
+ else hoverBarsUl.style.display = 'none';
+
+ const hasDl = p.dl_rssi !== null && p.dl_rssi !== undefined;
+ if (showDl && hasDl) { setGaugeRow('dl-', p.dl_rssi, p.dl_snr); hoverBarsDl.style.display = 'flex'; }
+ else hoverBarsDl.style.display = 'none';
+
hoverTip.style.left = (cx + 14) + 'px';
hoverTip.style.top = (cy - 14) + 'px';
hoverTip.style.display = 'block';
@@ -703,13 +733,20 @@ function showHoverTip(p, cx, cy) {
map.on('mousemove', e => {
const cursor = e.containerPoint;
+ const mode = OVERLAY_MODES[overlayModeIdx];
+ if (!mode.ul && !mode.dl) { hoverTip.style.display = 'none'; return; }
+ // Downlink-only mode: only bother matching points that actually have a downlink reading,
+ // so hovering finds whatever's really rendered on the downlink heat layer. In Uplink or
+ // Both mode, search all points — virtually everything has an uplink reading.
+ const dlOnly = mode.dl && !mode.ul;
let nearest = null, minDist = 30;
for (const p of allPoints) {
+ if (dlOnly && (p.dl_rssi === null || p.dl_rssi === undefined)) continue;
const pt = map.latLngToContainerPoint([p.lat, p.lon]);
const d = Math.hypot(pt.x - cursor.x, pt.y - cursor.y);
if (d < minDist) { minDist = d; nearest = p; }
}
- if (nearest && OVERLAY_MODES[overlayModeIdx].ul) showHoverTip(nearest, e.originalEvent.clientX, e.originalEvent.clientY);
+ if (nearest) showHoverTip(nearest, e.originalEvent.clientX, e.originalEvent.clientY, mode.ul, mode.dl);
else hoverTip.style.display = 'none';
});
@@ -1544,12 +1581,12 @@ def api_points():
con = get_db()
if from_ts is not None:
rows = con.execute(
- "SELECT lat, lon, rssi, snr, q, ts, sender_hash, source FROM points WHERE ts >= ? AND ts <= ? ORDER BY ts",
+ "SELECT lat, lon, rssi, snr, dl_rssi, dl_snr, q, ts, sender_hash, source FROM points WHERE ts >= ? AND ts <= ? ORDER BY ts",
(from_ts, to_ts if to_ts is not None else int(time.time())),
).fetchall()
else:
rows = con.execute(
- "SELECT lat, lon, rssi, snr, q, ts, sender_hash, source FROM points WHERE ts > ? ORDER BY ts",
+ "SELECT lat, lon, rssi, snr, dl_rssi, dl_snr, q, ts, sender_hash, source FROM points WHERE ts > ? ORDER BY ts",
(since,),
).fetchall()
con.close()
Served by rngit 1.4.2 - Generated in 0.02s